/**
 *   Copyright 2008 Alexey Vishentsev
 *
 *   Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 * 
 */

package com.gwtspreadsheetinput.jsf.component;

import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseId;

public class SpreadSheet extends UIComponentBase implements NamingContainer {
	
	public static final String COMPONENT_TYPE = 
			"com.gwtspreadsheetinput.jsf.SpreadSheet";

	public static final String COMPONENT_FAMILY = 
		"com.gwtspreadsheetinput.jsf.SpreadSheet";
	
	public static final State EMPTY_STATE = new State();
	
	
	private Object value;
	private SpreadSheetModel model;
	private String rowObjectClassName;
	private String var;
	
	private String width;
	private String height;
	
	private int scrollX;
	private int scrollY;
	
	private int selectionRow = -1;
	private int selectionCol = -1;
	private String jsFolder;
	
	private Object[] values;
	private Map<String,State> states;
	private int rowIndex = -1;
	
	
	public SpreadSheet() {
		super();
		setRendererType("com.gwtspreadsheetinput.jsf.SpreadSheetRenderer");
	}
	
	
	public String getFamily() {
		return COMPONENT_FAMILY;
	}
	

	public Object getValue() {
		if (this.value != null) {
			return value;
		}
		ValueExpression valueExpression = getValueExpression("value");
		if (valueExpression != null) {
			try {
				return valueExpression.getValue(getFacesContext().getELContext());
			} catch (ELException e) {
				throw new FacesException(e);
			}
		} else {
			return null;
		}
	}
	
	public void setValue(Object value) {
		setSpreadSheetModel(null);
		this.value = value;
	}

	
	protected SpreadSheetModel getSpreadSheetModel(){
		if (this.model != null) {
			return model;
		}
		Object val = getValue();
		Class rowObjectClass;
		try {
			rowObjectClass = Class.forName(rowObjectClassName);
		} catch (ClassNotFoundException e) {
			throw new IllegalStateException("Cannot find class: "
					+rowObjectClassName,e);
		}
		if (val == null) {
			new SpreadSheetListModel(rowObjectClass);
		} else if (val instanceof SpreadSheetModel) {
			setSpreadSheetModel((SpreadSheetModel) val);
		} else if (val instanceof List) {
			setSpreadSheetModel(new SpreadSheetListModel((List)val,rowObjectClass));
		}
		return this.model;
	}
	
	
	
	protected void setSpreadSheetModel(SpreadSheetModel model) {
		this.model = model;
	}
	
	public String getVar() {
		return var;
	}
	
	public void setVar(String var) {
		this.var = var;
	}
	
	public void setJsFolder(String jsFolder) {
		this.jsFolder = jsFolder;
	}
	
	public String getJsFolder() {
		return jsFolder;
	}
	
	public void setRowObjectClassName(String rowObjectClassName) {
		this.rowObjectClassName = rowObjectClassName;
	}
	
	public String getRowObjectClassName() {
		return rowObjectClassName;
	}
	
	public String getWidth() {
		return width;
	}
	
	public void setWidth(String width) {
		this.width = width;
	}
	
	public String getHeight() {
		return height;
	}
	
	public void setHeight(String height) {
		this.height = height;
	}
	
	public int getScrollX() {
		return scrollX;
	}
	
	public int getScrollY() {
		return scrollY;
	}
	
	public int getSelectionRow() {
		return selectionRow;
	}
	
	public int getSelectionCol() {
		return selectionCol;
	}
	
	public void setSelectionRow(int row) {
		this.selectionRow = row;
	}
	
	public void setSelectionCol(int col) {
		this.selectionCol = col;
	}
	
	public void setScroll(String scrollXValue, String scrollYValue) {
		scrollX = scrollY = 0;
		if (scrollXValue != null) {
			try {
				scrollX = Integer.parseInt(scrollXValue);
			} catch (NumberFormatException x) {
				//Ignore
			}
		}
		if (scrollYValue != null) {
			try {
				scrollY = Integer.parseInt(scrollYValue);
			} catch (NumberFormatException x) {
				//Ignore
			}
		}
	}
	
	@Override
	public String getContainerClientId(FacesContext context) {
		String clientId = super.getClientId(context);
		if (rowIndex < 0 ) {
			return clientId;
		} else {
			return clientId + NamingContainer.SEPARATOR_CHAR + rowIndex;
		}	
	}
	
	public void setRowIndex(int index) {
		if (this.rowIndex == index) {
			return;
		}
		FacesContext context = getFacesContext();
		SpreadSheetModel model = getSpreadSheetModel();
		if (this.rowIndex >= 0 && this.rowIndex < model.getRowCount() && 
				!model.isRowEmpty()) {
			
			for(UIComponent child: getChildren()) {
				if (child instanceof SpreadSheetColumn) {
					String id = child.getClientId(context);
					State state = states.get(id);
					if (state == null) {
						state = new State();
						states.put(id, state);
					}
					state.save((SpreadSheetColumn)child);
				}
			}
		}
		this.rowIndex = index;
		model.setRowIndex(index);
		if (var != null) {
			Map<String,Object> requestMap = context
				.getExternalContext().getRequestMap();
			
			if (model.isRowAvailable() && ! model.isRowEmpty()) {
				requestMap.put(var, model.getRowData());
			} else {
				requestMap.remove(var);
			}
		}
		//reset children ids and state
		for (UIComponent child:getChildren()) {
			if (child instanceof SpreadSheetColumn) {
				String id = child.getId();
				child.setId(id);
				EMPTY_STATE.restore((SpreadSheetColumn)child);
			}
		}
		if (this.rowIndex >= 0 && this.rowIndex < model.getRowCount() && !model.isRowEmpty()) {
			for(UIComponent child: getChildren()) {
				if (child instanceof SpreadSheetColumn) {
					String clientId = child.getClientId(context);
					State state = states.get(clientId);
					if (state == null) {
						continue;
					}
					state.restore((SpreadSheetColumn)child);
				}
			}
		}
	}
	
	public void makeRowEmpty(){
		 if (this.rowIndex < 0 ) {
			 return;
		 }
		 getSpreadSheetModel().makeRowEmpty();
		 FacesContext context = getFacesContext();
		 for(UIComponent child: getChildren()) {
			 if (child instanceof SpreadSheetColumn) {
				 String id = child.getClientId(context);
				 states.remove(id);
			 }
		 }
	}
	
	public void clearFromCurrentRow() {
		if (rowIndex < 0) {
			return;
		}
		getSpreadSheetModel().clearFromCurrentRow();
		
	}
	
	protected void process(FacesContext context, PhaseId phaseId){
		int row = 0;
		SpreadSheetModel model = getSpreadSheetModel();
		while(true) {
			setRowIndex(row++);
			if (!model.isRowAvailable()) {
				break;
			}
			if (model.isRowEmpty()){
				continue;
			}
			if (getChildCount() > 0) {
				for (UIComponent child:getChildren()) {
					if (child.isRendered() && child instanceof SpreadSheetColumn) {
						if (phaseId == PhaseId.PROCESS_VALIDATIONS) {
							((SpreadSheetColumn)child).processValidators(context);
						} else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) {
							child.processUpdates(context);
						}
					}
				}
			}
		}
		setRowIndex(-1);
	}
	
	@Override
	public void processDecodes(FacesContext context) {
		if (!isRendered()) {
			return;
		}
		//Re-evaluate
		setSpreadSheetModel(null);
		if (states == null) {
			states = new HashMap<String, State>();
		}
		decode(context);
	}
	
	@Override
	public void processUpdates(FacesContext context) {
		setRowIndex(-1);
		process(context,PhaseId.UPDATE_MODEL_VALUES);
	}
	
	@Override
	public void processValidators(FacesContext context) {
		setRowIndex(-1);
		process(context, PhaseId.PROCESS_VALIDATIONS);
	}
	
	@Override
	public void encodeBegin(FacesContext context) throws IOException {
		if (!contextHasErrorMessages(context)) {
			states = new HashMap<String, State>();
		}
		super.encodeBegin(context);
	}
	
	private boolean contextHasErrorMessages(FacesContext context) {
        FacesMessage.Severity sev = context.getMaximumSeverity();
        return (sev != null && (FacesMessage.SEVERITY_ERROR.compareTo(sev) >= 0));

    }
	
	@Override
	public Object saveState(FacesContext context) {
		if (values == null) {
			values = new Object[12];
		}
		values[0] = super.saveState(context);
		values[1] = width;
		values[2] = height;
		values[3] = new Integer(scrollX);
		values[4] = new Integer(scrollY);
		values[5] = new Integer(selectionRow);
		values[6] = new Integer(selectionCol);
		values[7] = rowObjectClassName;
		values[8] = states;
		values[9] = value;
		values[10] = var;
		values[11] = jsFolder;
		return values;
	}
	
	@Override
	public void restoreState(FacesContext context, Object state) {
		values = (Object[]) state;
		super.restoreState(context, values[0]);
		width = String.valueOf(values[1]);
		height = String.valueOf(values[2]);
		scrollX = ((Integer)values[3]).intValue();
		scrollY = ((Integer)values[4]).intValue();
		selectionRow = ((Integer)values[5]).intValue();
		selectionCol = ((Integer)values[6]).intValue();
		rowObjectClassName = (String) values[7];
		states = (Map<String,State>) values[8];
		value = values[9];
		var = (String) values[10];
		jsFolder = (String) values[11];
	}
	
	private static class State implements Serializable{
		private static final long serialVersionUID = -8973405967948104411L;
		private Object submittedValue;
		private Object value;
		private boolean valid;
		private boolean localValueSet;
		
		public State(){
			super();
		}
		
		public void save(EditableValueHolder input) {
			setSubmittedValue(input.getSubmittedValue());
			setValue(input.getValue());
			setValid(input.isValid());
			setLocalValueSet(input.isLocalValueSet());
		}
		
		public void restore(EditableValueHolder input) {
			input.setSubmittedValue(getSubmittedValue());
			input.setValue(getValue());
			input.setValid(isValid());
			input.setLocalValueSet(isLocalValueSet());
		}
		
		public Object getSubmittedValue() {
			return submittedValue;
		}
		public void setSubmittedValue(Object submittedValue) {
			this.submittedValue = submittedValue;
		}
		public Object getValue() {
			return value;
		}
		public void setValue(Object value) {
			this.value = value;
		}
		public boolean isValid() {
			return valid;
		}
		public void setValid(boolean valid) {
			this.valid = valid;
		}
		public boolean isLocalValueSet() {
			return localValueSet;
		}
		public void setLocalValueSet(boolean localValueSet) {
			this.localValueSet = localValueSet;
		}
	}

	
}
